package mapper

import (
	"context"
	"encoding/json"
	"errors"
	"sort"
	"time"

	clientv3 "go.etcd.io/etcd/client/v3"
	"gorm.io/gorm"
)

type ListOptions struct {
	Order  string
	Filter any
}

type PageListOptions struct {
	PageSize int
	PageNum  int
	Order    string
	Filter   any
}

type DataMapper[T any] interface {
	Save(context.Context, *T) error
	Insert(context.Context, *T) error
	Delete(context.Context, any) error
	Updates(context.Context, *T) error
	Query(context.Context, any) (*T, error)
	QueryByPrimaryKeys(context.Context, ...any) ([]*T, error)
	List(context.Context, ListOptions) ([]*T, error)
	PageList(context.Context, PageListOptions) (int64, []*T, error)
}

type TableModel interface {
	TableName() string
}

type Updatable interface {
	SetUpdateTime()
}

type Creatable interface {
	SetCreateTime()
}

type GormMapper[T TableModel] struct {
	db *gorm.DB
}

func NewGormMapper[T TableModel](db *gorm.DB) *GormMapper[T] {
	return &GormMapper[T]{
		db: db,
	}
}

func (b *GormMapper[T]) Insert(ctx context.Context, t *T) error {
	return b.db.WithContext(ctx).Create(t).Error
}

func (b *GormMapper[T]) Delete(ctx context.Context, t any) error {
	var tb T
	return b.db.WithContext(ctx).Table(tb.TableName()).Delete(t).Error
}

func (b *GormMapper[T]) Save(ctx context.Context, t *T) error {
	return b.db.WithContext(ctx).Save(t).Error
}

func (b *GormMapper[T]) Updates(ctx context.Context, t *T) error {
	return b.db.WithContext(ctx).Updates(t).Error
}

func (b *GormMapper[T]) Query(ctx context.Context, query any) (*T, error) {
	var t T
	if err := b.db.WithContext(ctx).Table(t.TableName()).Where(query).First(&t).Error; err != nil {
		return nil, err
	}
	return &t, nil
}

func (b *GormMapper[T]) QueryByPrimaryKeys(ctx context.Context, pks ...any) ([]*T, error) {
	var t T
	rlt := make([]*T, 0)
	if err := b.db.WithContext(ctx).Table(t.TableName()).Where("id in (?)", pks).Scan(&rlt).Error; err != nil {
		return nil, err
	}
	return rlt, nil
}

func (b *GormMapper[T]) List(ctx context.Context, options ListOptions) ([]*T, error) {
	var t T
	rlt := make([]*T, 0)

	if err := b.db.WithContext(ctx).Table(t.TableName()).Where(options.Filter).Order(options.Order).Scan(&rlt).Error; err != nil {
		return nil, err
	}
	return rlt, nil
}

func (b *GormMapper[T]) PageList(ctx context.Context, options PageListOptions) (int64, []*T, error) {
	var t T
	rlt := make([]*T, 0)
	count := int64(0)

	err := b.db.WithContext(ctx).Table(t.TableName()).Where(options.Filter).Count(&count).Error

	if err != nil {
		return 0, nil, err
	}

	err = b.db.WithContext(ctx).Table(t.TableName()).Where(options.Filter).
		Limit(options.PageSize).Offset((options.PageNum - 1) * options.PageSize).
		Order(options.Order).Scan(&rlt).Error

	if err != nil {
		return 0, nil, err
	}
	return count, rlt, nil
}

var (
	unsupportedType = errors.New("unsupported type for deletion")
)

type EtcdModel interface {
	GetKey() string
	GetExpiration() time.Duration
	GetPrefix() string
}

type EtcdMapper[T EtcdModel] struct {
	client *clientv3.Client
}

func (e *EtcdMapper[T]) Save(ctx context.Context, t *T) error {
	key := (*t).GetKey()
	value, err := json.Marshal(t)
	if err != nil {
		return err
	}

	if (*t).GetExpiration() == 0 {
		_, err = e.client.Put(ctx, key, string(value))
	} else {
		leaseResp, err := e.client.Grant(ctx, int64((*t).GetExpiration().Seconds()))
		if err != nil {
			return err
		}
		_, err = e.client.Put(ctx, key, string(value), clientv3.WithLease(leaseResp.ID))
	}

	if err != nil {
		return err
	}
	return nil
}

func (e EtcdMapper[T]) Insert(ctx context.Context, t *T) error {
	key := (*t).GetKey()
	value, err := json.Marshal(t)
	if err != nil {
		return err
	}

	if (*t).GetExpiration() == 0 {
		_, err = e.client.Put(ctx, key, string(value))
	} else {
		leaseResp, err := e.client.Grant(ctx, int64((*t).GetExpiration().Seconds()))
		if err != nil {
			return err
		}
		_, err = e.client.Put(ctx, key, string(value), clientv3.WithLease(leaseResp.ID))
	}

	if err != nil {
		return err
	}
	return nil
}

// a string(key) or etcdmodel
func (e *EtcdMapper[T]) Delete(ctx context.Context, a any) error {
	var key string
	switch v := a.(type) {
	case string:
		key = v
	case EtcdModel:
		key = v.GetKey()
	default:
		return unsupportedType
	}
	_, err := e.client.Delete(ctx, key)
	return err
}

func (e EtcdMapper[T]) Updates(ctx context.Context, t *T) error {
	key := (*t).GetKey()
	value, err := json.Marshal(t)
	if err != nil {
		return err
	}

	if (*t).GetExpiration() == 0 {
		_, err = e.client.Put(ctx, key, string(value))
	} else {
		leaseResp, err := e.client.Grant(ctx, int64((*t).GetExpiration().Seconds()))
		if err != nil {
			return err
		}
		_, err = e.client.Put(ctx, key, string(value), clientv3.WithLease(leaseResp.ID))
	}

	if err != nil {
		return err
	}
	return nil
}

func (e EtcdMapper[T]) Query(ctx context.Context, a any) (*T, error) {
	var key string

	switch v := a.(type) {
	case string:
		key = v
	case EtcdModel:
		key = v.GetKey()
	default:
		return nil, unsupportedType
	}

	resp, err := e.client.Get(ctx, key)
	if err != nil {
		return nil, err
	}

	if len(resp.Kvs) == 0 {
		return nil, nil
	}

	value := resp.Kvs[0].Value

	var t T
	err = json.Unmarshal(value, &t)
	if err != nil {
		return nil, err
	}

	return &t, nil
}

func (e EtcdMapper[T]) QueryByPrimaryKeys(ctx context.Context, a ...any) ([]*T, error) {

	var keys []string

	for _, param := range a {
		switch v := param.(type) {
		case string:
			keys = append(keys, v)
		case EtcdModel:
			keys = append(keys, v.GetKey())
		default:
			return nil, unsupportedType
		}
	}

	results := make([]*T, 0)

	for _, key := range keys {
		resp, err := e.client.Get(ctx, key)
		if err != nil {
			return nil, err
		}

		if len(resp.Kvs) > 0 {
			value := resp.Kvs[0].Value

			var t T
			err := json.Unmarshal(value, &t)
			if err != nil {
				return nil, err
			}

			results = append(results, &t)
		}
	}

	return results, nil
}

func (e EtcdMapper[T]) List(ctx context.Context, options ListOptions) ([]*T, error) {
	var key string
	var t T

	if options.Filter == nil {
		key = t.GetPrefix()
	} else {
		switch v := options.Filter.(type) {
		case string:
			key = v
		case EtcdModel:
			key = v.GetKey()
		default:
			return nil, unsupportedType
		}
	}

	resp, err := e.client.Get(ctx, key, clientv3.WithPrefix())
	if err != nil {
		return nil, err
	}

	results := make([]*T, 0)

	for _, kv := range resp.Kvs {
		value := kv.Value

		var t T
		err := json.Unmarshal(value, &t)
		if err != nil {
			return nil, err
		}

		results = append(results, &t)
	}

	if options.Order == "desc" {
		sort.Slice(results, func(i, j int) bool {
			return (*results[i]).GetKey() > (*results[j]).GetKey()
		})
	} else {
		sort.Slice(results, func(i, j int) bool {
			return (*results[i]).GetKey() < (*results[j]).GetKey()
		})
	}

	return results, nil
}

func (e EtcdMapper[T]) PageList(ctx context.Context, options PageListOptions) (int64, []*T, error) {
	var key string

	var t T

	if options.Filter == nil {
		key = t.GetPrefix()
	} else {
		switch v := options.Filter.(type) {
		case string:
			key = v
		case EtcdModel:
			key = v.GetKey()
		default:
			return 0, nil, unsupportedType
		}
	}

	resp, err := e.client.Get(ctx, key, clientv3.WithPrefix())
	if err != nil {
		return 0, nil, err
	}

	totalCount := resp.Count
	kvs := resp.Kvs

	if options.Order == "desc" {
		sort.Slice(kvs, func(i, j int) bool {
			return string(kvs[i].Key) > string(kvs[j].Key)
		})
	} else {
		sort.Slice(kvs, func(i, j int) bool {
			return string(kvs[i].Key) < string(kvs[j].Key)
		})
	}

	startIndex := (options.PageNum - 1) * options.PageSize
	endIndex := startIndex + options.PageSize

	results := make([]*T, 0, options.PageSize)
	for i := startIndex; i < len(kvs) && i < endIndex; i++ {
		value := kvs[i].Value

		var t T
		err := json.Unmarshal(value, &t)
		if err != nil {
			return 0, nil, err
		}

		results = append(results, &t)
	}

	return totalCount, results, nil
}

func NewEtcdMapper[T EtcdModel](client *clientv3.Client) *EtcdMapper[T] {
	return &EtcdMapper[T]{
		client: client,
	}
}
